<?php
class MysqlAdapter extends SqlAdapter
{
    protected static $openedConnections = array();
    protected $quote = '`';

    protected function MysqlAdapter($connection_id)
    {
        parent::__construct($connection_id);
        if ($connection_id == AppConfig::GetInstance()->DB_INFORMATION_SCHEMA)
        {
            $db = ConnectionModel::GetInstance()->GetDefault();
            $db['database'] = $connection_id;
        }
        else
        {
            $db = ConnectionModel::GetInstance()->FindById($connection_id);
        }
        $this->driver = new mysqli($db['host'], $db['login'], $db['password'], $db['database']);
        $this->driver->set_charset($db['encoding']);
        self::Add($this->name, $this);
    }

    public static function Get($connection_id)
    {
        if (!self::IsExisted($connection_id))
            new MysqlAdapter($connection_id);
        return self::$openedConnections[$connection_id];
    }

    protected static function Add($connection_id, $conn)
    {
        self::$openedConnections[$connection_id] = $conn;
    }

    protected static function Remove($connection_id)
    {
        unset(self::$openedConnections[$connection_id]);
    }

    protected static function IsExisted($connection_id)
    {
        return isset(self::$openedConnections[$connection_id]);
    }

    public static function CloseAll()
    {
        foreach (self::$openedConnections as $conn)
        {
            $conn->Close();
        }
    }

    public static function GetInformationSchema()
    {
        return self::Get(AppConfig::GetInstance()->DB_INFORMATION_SCHEMA);
    }

    public static function GetDefault()
    {
        return self::Get(ConnectionModel::GetDefaultId());
    }

    public function Close()
	{
        if ($this->transactionId)
            throw new Exception("Active transaction {$this->transactionId} is not commited before closing connection");
        $this->driver->close();
        self::Remove($this->name);
	}

    public function GetInsertId()
    {
        return $this->driver->insert_id;
    }

    public function GetAffectedRows()
    {
        return $this->driver->affected_rows;
    }

    protected function FetchResult($result)
    {
        $output = array();
        while ($row = $result->fetch_assoc())
        {
            $output[] = $row;
        }
        return $output;
    }

    public function ExecuteMulti($sql)
    {
        return $this->driver->multi_query($sql);
    }

    public function Select($table_list, $fields = null, $condition = null, $join = null, $orderby = null, $limit = null, &$page = 1, &$count = null, $groupby = null, $force_indexes = null)
    {
        Log::LogQueryType('SELECT', $table_list);

        $this->AddQuotes($table_list);
        $force_indexes = $this->GetForceIndexes($force_indexes);
        $fields = $this->GetSelectFields($fields);
        $where = $this->GetWhere($condition);
        $groupby = $this->GetGroupBy($groupby);
        $orderby = $this->GetOrderBy($orderby);

        $from = "$table_list
        $force_indexes
        $join
        $where";

        $select = "SELECT $fields
        FROM $from
        $groupby";

        $count = $this->GetCount($count, $groupby, $select, $from);
        $limit = $this->GetSelectLimit($limit, $page, $count);

        return $this->Query(
            "$select
            $orderby
            $limit");
    }

    protected function GetForceIndexes($force_indexes)
    {
        if (is_array($force_indexes))
            $force_indexes = implode(',', $force_indexes);
        return empty($force_indexes) ? null : "FORCE INDEX ($force_indexes)";
    }

    public function GetForeignKeys($table)
    {
        $fks = array();
        $sql = $this->ShowCreateTable($table);
        preg_match_all('/CONSTRAINT.+FOREIGN KEY.+/', $sql, $matches);
        $fks = array();
        foreach($matches[0] as $match)
        {
            preg_match_all('/`(.+)`/U', $match, $m);
            $mm = $m[1];
            $fks[$mm[1]] = new SqlForeignkey($mm[0], $mm[2], $mm[3]);
        }
        return $fks;
    }

    public function GetFKColumns($table)
    {
        $fks = array();
        $sql = $this->ShowCreateTable($table);
        preg_match_all('/CONSTRAINT.+FOREIGN KEY.+/', $sql, $matches);
        $fks = array();
        foreach($matches[0] as $match)
        {
            preg_match_all('/`(.+)`/U', $match, $m);
            $mm = $m[1];
            $i['column'] = $mm[1];
            $i['ref_column'] = $mm[3];
            $fks[$mm[0]] = $i;
        }
        return $fks;
    }

    public function RollbackTransaction($transaction_id)
    {
        if ($transaction_id == $this->transactionId)
        {
            $this->transactionId = null;
            return $this->Execute("ROLLBACK -- $transaction_id");
        }
    }

    public function Describe($table)
    {
        $this->AddQuotes($table);
        return $this->Query("DESCRIBE $table");
    }

    public function ShowCreateTable($table)
    {
        $this->AddQuotes($table);
        $data = $this->Query("SHOW CREATE TABLE $table");
        return $data[0]['Create Table'];
    }

    public function ShowTables()
    {
        $tables = $this->Query('SHOW TABLES');
        foreach ($tables as $table)
        {
            $out[] = array_shift($table);
        }
        return $out;
    }

    public function EscapeString($string)
    {
        return $this->driver->escape_string($string);
    }

    public function GetFields($table)
    {
        $out = array();
        $fields = $this->Describe($table);
        foreach ($fields as $field)
        {
            $f['name']      = $field['Field'];
            $f['type']      = $field['Type'];
            $f['nullable']  = $field['Null']=='YES';
            $f['is_pk']     = $field['Key']=='PRI';
            $f['default']   = $field['Default'];
            $f['auto_increment'] = $field['Extra'] == 'auto_increment';
            $out[] = $f;
        }
        return $out;
    }

    public function GetLastError()
    {
        return $this->driver->errno;
    }

    public function GetErrorMessage()
    {
        return $this->driver->error;
    }

    public function AlterSchemaCharset($schema, $charset)
    {
        $charset = $this->GetSqlCharset($charset);
        $this->AddQuotes($schema);
        return $this->Execute("ALTER DATABASE $schema $charset");
    }

    public function AlterSchemaCollation($schema, $collation)
    {
        $collation = $this->GetSqlCollation($collation);
        $this->AddQuotes($schema);
        return $this->Execute("ALTER DATABASE $schema $collation");
    }

    public function DropSchema($schema)
    {
        $query = 'DROP DATABASE';
        Log::LogQueryType($query, $schema);
        $this->AddQuotes($schema);
        return $this->Execute("$query $schema");
    }

    public function CreateSchema($schema, $charset = null, $collation = null)
    {
        if ($charset)
        {
            $charset = $this->GetSqlCharset($charset);
            $collation = null;
        }
        else if ($collation)
        {
            $charset = null;
            $collation = $this->GetSqlCollation($collation);
        }
        $this->AddQuotes($schema);
        return $this->Execute("CREATE DATABASE $schema $charset $collation");
    }

    public function CreateTable($table, $engine, $collation, $auto_increment, $comment, $fields)
    {
        $this->AddQuotes($table);
        $columns = $this->GetSqlColumns($fields);
        $table_options = $this->GetSqlTableOptions($engine, $collation, $auto_increment, $comment);

        $sql = "CREATE TABLE $table (\n\t$columns) $table_options";
        return $this->Execute($sql);
    }

    public function AlterTable($table, $rename, $engine, $collation, $auto_increment, $comment)
    {
        $this->AddQuotes($table);
        $rename = $this->GetSqlRenameTable($rename);
        $table_options = $this->GetSqlTableOptions($engine, $collation, $auto_increment, $comment);
        if (trim($rename) && trim($table_options))
            $table_options = ', '.$table_options;

        $sql = "ALTER TABLE $table $rename $table_options";
        return $this->Execute($sql);
    }

    public function GetSqlTableOptions($engine, $collation, $auto_increment, $comment)
    {
        $engine = $this->GetSqlEngine($engine);
        $collation = $this->GetSqlCollation($collation);
        $auto_increment = $this->GetSqlAutoIncrement($auto_increment);
        $comment = $this->GetSqlComment($comment);
        return "$engine $auto_increment $collation $comment";
    }

    protected function GetSqlRenameTable($rename)
    {
        if ($rename)
        {
            $this->AddQuotes($rename);
            return "RENAME TO $rename";
        }
    }

    protected function GetSqlEngine($engine)
    {
        return $engine ? "ENGINE=$engine" : null;
    }

    protected function GetSqlAutoIncrement($auto_increment)
    {
        return is_null($auto_increment) ? null : "AUTO_INCREMENT=$auto_increment";
    }

    protected function GetSqlComment($comment)
    {
        return is_null($comment) ? null : "COMMENT '$comment'";
    }

    protected function GetSqlCharset($charset)
    {
        return $charset ? "DEFAULT CHARACTER SET=$charset" : null;
    }

    protected function GetSqlCollation($collation)
    {
        return $collation ? "DEFAULT COLLATE=$collation" : null;
    }

    public function DropTable($table)
    {
        $query = 'DROP TABLE';
        Log::LogQueryType($query, $table);
        $this->AddQuotes($table);
        return $this->Execute("$query $table");
    }

    public function AddColumn($table, $field)
    {
        $this->AddQuotes($table);
        $column = $field->GetSql();
        $sql = "ALTER TABLE $table ADD COLUMN $column";
        return $this->Execute($sql);
    }

    public function ModifyColumn($table, $field)
    {
        $this->AddQuotes($table);
        $column = $field->GetUpdateSql();
        $sql = "ALTER TABLE $table $column";
        return $this->Execute($sql);
    }

    public function DropColumn($table, $column)
    {
        $this->AddQuotes($table);
        $this->AddQuotes($column);
        return $this->Execute("ALTER TABLE $table DROP $column");
    }

    public function ShowEngines()
    {
        return $this->Query("SHOW ENGINES");
    }

    public function SelectSchema($schema)
    {
        return $this->driver->select_db($schema);
    }

    public function AddPrimaryKey($table, $primary_keys, $drop = true)
    {
        if (empty($primary_keys))
            $this->DropPrimaryKey($table);
        else
        {
            $drop = $drop ? 'DROP PRIMARY KEY,' : null;
            $this->AddQuotes($table);
            $this->AddQuotes($primary_keys);
            $sql = "ALTER TABLE $table $drop ADD PRIMARY KEY($primary_keys)";
            return $this->Execute($sql);
        }
    }

    public function DropPrimaryKey($table)
    {
        $this->AddQuotes($table);
        $sql = "ALTER TABLE $table DROP PRIMARY KEY";
        return $this->Execute($sql);
    }

    public function StartTransaction($transaction_id)
    {
        if (!$this->transactionId)
        {
            $this->transactionId = $transaction_id;
            return $this->Execute("START TRANSACTION -- $transaction_id");
        }
    }

    public function DropIndex($table, $index)
    {
        $this->AddQuotes($table);
        $this->AddQuotes($index);
        return $this->Execute("ALTER TABLE $table DROP INDEX $index");
    }

    public function CreateIndex($index, $table, $column, $type = null)
    {
        if (!$index)
            $index = "IX_$table"."_$column";

        $this->AddQuotes($table);
        $this->AddQuotes($column);
        $this->AddQuotes($index);
        return $this->Execute("CREATE $type INDEX $index ON $table ($column)");
    }

    public function DropForeignKey($table, $key)
    {
        $this->AddQuotes($table);
        $this->AddQuotes($key);
        $this->Execute("ALTER TABLE $table DROP FOREIGN KEY $key");
        return $this->DropIndex($table, $key);
    }

    public function AddForeignKey($key, $table, $colum, $ref_table, $ref_column)
    {
        if (!$key)
            $key = "FK_$table"."__$colum";

        $this->AddQuotes($key);
        $this->AddQuotes($table);
        $this->AddQuotes($colum);
        $this->AddQuotes($ref_table);
        $this->AddQuotes($ref_column);
        return $this->Execute("ALTER TABLE $table ADD CONSTRAINT $key FOREIGN KEY $key($colum) REFERENCES $ref_table($ref_column)");
    }

    public function GetSqlColumns($fields)
    {
        $pk = ModelField::GetPrimaryKey($fields);
        $columns = array();
        foreach ($fields as $field)
        {
            $columns[] = $field->GetSql();
        }
        $columns[] = "PRIMARY KEY (`$pk`)";
        return implode(",\n\t", $columns);
    }
}
?>